[Ops] Use block matrix inversion to speed up solve_tril for the Ascend NPU backend - #1145
[Ops] Use block matrix inversion to speed up solve_tril for the Ascend NPU backend#1145OsirisDuan wants to merge 2 commits into
Conversation
…l and simplify kernel initialization - Added input validation for solve_tril_verifier; limits BT to 16/32/64 - The kernel uses direct pointer arithmetic and masks instead of block_ptr; removes NT_OFFSET/BH_OFFSET - Diagonal block inversion uses MBH block iteration method; variable-length tail reverts to row-by-row iteration - Merged the kernel implementation to recursively perform 2x2 block inversion, eliminating row-by-row loops - Removed _launch_solve_tril_kernel; directly launch the kernel using a 2D grid - Added USE_TMA and DOT_PRECISION parameters to all kernels, and enabled IS_VARLEN heuristic
| b_Ai_21 = -tl.dot(tl.dot(b_Ai_22, b_A_21, input_precision=DOT_PRECISION), b_Ai_11, input_precision=DOT_PRECISION) | ||
|
|
||
| p_Ai_11 = Ai + o_t[:, None] * (H*BT) + o_i[None, :] | ||
| p_Ai_21 = Ai + (o_t[:, None] + 16) * (H*BT) + o_i[None, :] | ||
| p_Ai_22 = Ai + (o_t[:, None] + 16) * (H*BT) + (o_i[None, :] + 16) | ||
| tl.store(p_Ai_11, b_Ai_11.to(p_Ai_11.dtype.element_ty, fp_downcast_rounding="rtne"), mask=(o_t[:, None] < T)) | ||
| tl.store(p_Ai_22, b_Ai_22.to(p_Ai_22.dtype.element_ty, fp_downcast_rounding="rtne"), mask=(o_t[:, None] + 16 < T)) |
There was a problem hiding this comment.
The 32×32 / 64×64 kernels drop the + 0.0 lhs copies that the current NPU solve_tril already has. On Ascend, tl.dot(lhs, rhs) can overwrite lhs in UB; later reads (second lhs, rhs, store, or X = X + tl.dot(X, Y)) see the clobbered tile. This file is in the repo catalog for exactly that (b_Ai_22_c on 32×32; b_Ai_22_c / b_Ai_33_c{,2} / b_Ai_44_c{,2} / b_A_42_c / b_A_43_c on 64×64).
This site is the clear example: inner tl.dot(b_Ai_22, b_A_21) then tl.store(..., b_Ai_22) on L157. The 64×64 path is the same class of bug, denser (tl.dot(A, A), X = X + tl.dot(X, Y), lhs reused in b_Ai_31/b_Ai_32 and b_Ai_41/b_Ai_42).
Please restore copies before the first lhs tl.dot of each reused tile. A 0.0 assert_close on test_solve_tril does not show the compiler stopped clobbering; that failure is silent and layout-dependent.
|
@OsirisDuan #1161 has landed ( |
Summary
Rewrites the Ascend NPU
solve_trilkernels to align with the GPU mainline on signature, index width, and memory access form, while adopting an NPU-friendlier block-inversion algorithm. This is an in-backend implementation replacement fortriton_ascend: no wrapper signature change, no registration change, no observable behavior change.Changes (
fla/ops/utils/backends/triton_ascend/solve_tril.py+__init__.py, +234/-197):Kernel signature aligned with GPU mainline. The three kernels (
solve_tril_16x16_kernel_npu/merge_16x16_to_32x32_inverse_kernel_npu/merge_16x16_to_64x64_inverse_kernel_npu) now share(A, Ai, cu_seqlens, chunk_indices, T, H, BT, USE_TMA, IS_VARLEN, DOT_PRECISION)withH/BT/USE_TMA/IS_VARLEN/DOT_PRECISIONastl.constexpr,@triton.heuristicsforIS_VARLEN, anddo_not_specialize=['T']. The previously mergedNT_OFFSET/BH_OFFSET(and the grid-split mechanism they enabled) are removed.Index width int32 -> int64 (correctness fix).
program_idis cast to int64; base address(bos*H+i_h)*BTand row offset(i_t*BT+i)*H*BTstay int64 end-to-end; in the varlen branchi_nis cast to int32 andi_t/cu_seqlensto int64. The old int32 indexing could overflow for largeB*T*Hcombos (e.g. B=16, T=131072, H=64, BT=64 -> base offset ~= 8.05e9 > 2^31).Memory access:
tl.make_block_ptr-> pointer arithmetic + mask.make_block_ptronly accepts int32 offsets, which conflicts with the int64 indices above (AssertionError: Block pointers only support 32 bit offsets/block_shape). Replaced with the GPU-mainline-styleo_t = (i_t*BT + o_i).to(tl.int64)+p = base + o_t[:,None]*stride + ...+tl.load(..., mask=...).[Algorithm change declaration] Diagonal block inversion: serial forward-substitution ->
tri_inv_mchdoubling method. For the four 16x16 diagonal blocks of the 64x64 kernel, replaces GPU mainline's serial row-by-row forward substitution (15-level vector dependency chain) with thetri_inv_mchdoubling iteration (3-level Cube dependency chain, higher NPU parallelism). The two methods are mathematically equivalent under exact arithmetic (Astrictly lower-triangular -> nilpotent -> finite convergent series,Sum_{k=0}^{15} B^k = (I - B)^{-1}); the only difference is fp rounding order. The varlen tail short-block path (remaining_rows < 16/32/48/64) keeps the serial fallback branch for boundary correctness.Off-diagonal blocks: recursive 2x2 grouping. The 64x64 off-diagonal blocks use recursive 2x2 block inversion (reusing
P00/P01/P10/P11intermediates, 11 dots) instead of the GPU mainline expansion (9 dots). Mathematically equivalent; dependency tree is shallower. Total 16x16 dot count is identical (16).Wrapper simplified. Removes
_launch_solve_tril_kerneland the grid-split machinery; reverts to a singlemerge_fn[NT, B*H](...)launch withUSE_TMA=False, DOT_PRECISION="ieee", and drops the triton-ascend extension launch args (num_stages/multibuffer/sync_solver).Verifier hardened.
solve_tril_verifiernow validatesIS_NPU,A.device.type == 'npu',A.dtype in {fp16, bf16, fp32}, andA.shape[-1] in {16, 32, 64}instead of unconditionally returningTrue, None.Test plan
tests/ops/test_solve_tril.pypython -m pytest tests/ops/test_solve_tril.py -v -p no:cacheproviderdiff/ratioall0.000000cu_seqlens=[0,15], multi-segment[0,256,500,1000],[0,1,100,300,1200,2048])test_solve_tril_large_batch_offsets(Blackwell-only, expected on NPU)python scripts/find_dependent_tests.py fla/ops/utils/backends/triton_ascend/solve_tril.pyreturns 47 files (test_gdn,test_gla,test_linear_attn,test_kda, ...). Not run in full here (too long);solve_trilis a utils op consumed by those kernels, recommended for CI coverage.Benchmark / NCU (kernel changes only)
<TBD>Breaking changes
None.
solve_tril_npuwrapper signature(A, cu_seqlens, chunk_indices, output_dtype)is unchanged;TritonAscendUtilsBackend.solve_trilregistration in__init__.pyis unchanged; output semantics ((I+A)^{-1}, shape/dtype) are unchanged. The algorithm change (item 4) shows no numerical difference within test tolerance.Checklist
If you cannot tick the "not minor" box above
Standalone minor PRs are normally not accepted (see No busywork PRs).
If you believe yours is an exception, justify here why it is worth a maintainer's review time — PRs without a justification may be closed without review: